"""
CPM/Indeы: Описание пакетного индекса Packages
Author: Milinuri Nirvalen
Ver: 2.0
"""

from screen import Logger, select
from utils import path_iterator, ask_user
from package import Package
from config import Config, load_file

import os
import hashlib
import fnmatch

# used/

log = Logger('Index')


def get_file_hash(path):
    """Получает md5 хеш файла по его пути."""
    if not os.path.exists(path):
        return ""

    with open(path, 'rb') as f:
        return hashlib.md5(f.read()).hexdigest()

def get_package_files(path):
    """получает словарь файлов пакетов, типа {путь: хеш},
    
    :param path: Начальный путь для поиска пакетов

    :return: Словарь (путь: хеш)"""
    
    res = {}
    places = [path]

    for place in path_iterator(places):
        if len(places) > 1 and 'packages.toml' in os.listdir(place):
            continue

        for x in os.listdir(place):
            p = os.path.join(place, x)

            if os.path.isdir(p):
                places.append(p)

            else:
                res[p] = get_file_hash(p)

    return res


class PackagesIndex:
    """Описание индекса пакетов.
    Управление пакетами на более высоком уровне.

    :param data: Содержимое файла индекса пакетов
    :param xonfig: Настройки cpm"""

    def __init__(self, data, config=None):
        super(PackagesIndex, self).__init__()
        self.data = data
        self.packages = {}

        if not config:
            self.config = Config()
        else:
            self.config = config

    
    # Добавление и обработка данных индекса
    # =====================================

    def extended_info(self):
        """Собирает расширенную информацию о пакетах. 
        Ищет проблемы с пакетами."""
        
        log.group('Поиск расширенной информации...')
        used = {}

        for k, v in self.data.items():
            for x in v['require']:
                if isinstance(x, list):
                    x = select(x)

                if x == k:
                    log.warn(f'{k}: Обнаружена зависимость на себя')
                    continue

                if x not in self.data:
                    log.warn(f'{k}: Не найдена зависимость {x}')
                    continue

                p = self.get_package(x)
                
                if not p.is_installed():
                    continue

                if x not in used:
                    used[x] = []

                used[x].append(k)
                
        for k, v in used.items():
            p = f'\033[93m{k}\033[0m'
            log.info(f'{p} используют \033[32m{len(v)} пакетов\033[0m')
            self.data[k]["used"] = v

    def add_package(self, p):
        """Добавляет пакет в индекс.
        :param p: экзмепляр Package"""

        path = p.data["path"].strip('./')

        # Если не указаны файлы - ищем
        if not p.data["files"]:
            p.data["files"] = get_package_files(path)
            
        # Если указан список фалов - получаем их хеши
        elif isinstance(p.data["files"], list):
            hashes = {}

            if not 'packages.toml' in p.data["files"]:
                p.data["files"].append('packages.toml')

            files = map(lambda x: os.path.join(path, x), p.data["files"])
            for file in files:
                hashes[file] = get_file_hash(file)

            p.data["files"] = hashes

        if not p.is_installed():
            return

        if p.pid in self.data:
            log.warn(f'Одинаковый PID для пакетов: {p.pid}')
            p.pid = p.pid + "~"

        self.data[p.pid] = p.data
        self.packages[p.pid] = p.data

        m_str = "\033[36mM:\033[0m " if p.data["multiple"] else ""
        log.complite(f'{m_str}{p.pid}: {path}')

    def get_require(self, args):
        """Получает все зависимости пакетов.
        
        :param args: Список PID, для которых нужно получить зависимости

        :returns: Список PID с зависимостями"""
        
        res = []

        for x in args:
            if isinstance(x, list):
                x = select(x)

            if x in res or x == "*":
                continue

            if x not in self.data:
                log.warn(f'{x}: не улалось получить зависимости')
                continue

            res.append(x)
            args += self.data[x]["require"]
        return res

    def count_types(self):
        """Считает пакеты по их типам.
        :return: Словарь {тип: количество}"""

        res = {'all': len(self.data)}
        for k, v in self.data.items():
            res[v["type"]] = res.get(v["type"], 0) + 1

        return res


    # Поиск локальных пакетов
    # =======================

    def compile(self):
        """Ищет локальные пакеты и обновляет индекс."""
        
        res = {}
        places = ['.']
        log.group('Сборка пакетного индекса...')

        # Проходимся по папкам
        for place in path_iterator(places):
            for x in os.listdir(place):
                path = os.path.join(place, x)

                # Добавляем новые папки для поиска
                if os.path.isdir(path):
                    places.append(path)
                    continue

                # Если найден нужный файл, добавляем
                if x == 'packages.toml':
                    packages = load_file(path)

                    multiple = False

                    if len(packages) > 1:
                        multiple = True

                    for k, v in packages.items():
                        if len(packages) > 1:
                            pid = place+"/"+k
                        else:
                            pid = place

                        pid = pid.strip("./")

                        if pid in self.config["ignore_pid"]:
                            log.info(f'Пропуск {pid}')
                            continue

                        if self.config["split_pid"]:
                            pid = pid.split('/', self.config["split_pid"])[-1]

                        p = Package(pid, v, self.config)
                        p.data["path"] = place
                        p.data["multiple"] = multiple
                        self.add_package(p)

        self.extended_info()


    # Получение пакетаов из индекса
    # =============================

    def get_package(self, pid, data={}):
        """Возвращает Package экземпляр по данным пакета.
        Если пакет есть в индесе, вернёт его данные.
        
        :param pid: package ID
        :param data: Данные пакета

        :return: Экземпляр Package"""

        if pid in self.data:
            return Package(pid, self.data[pid], self.config)

        elif data:
            return Package(pid, data, self.config)

        return Package('nodata', {}, self.config)

    def get_packages_by_pid(self, names=[], optional=True):
        """Более гибкий спосб получения PID пакетов.
        Ищет пакеты по примерным названиям и прдоставляет вам выбор.

        :param names: Список шаблонов для поиска
        :param optiona: Является ли выбор пакетов необязательным

        :returm: Список PID пакетов"""
        
        packages = []

        for n in names:
            res = []

            # Поиск пакетов по PID
            for k, v in self.data.items():
                if k.count('.'):
                    pk = k.split('.', 1)[1]
                
                else:
                    pk = k

                if fnmatch.fnmatch(pk, n) or n in pk:
                    res.append(k)

            # Обработка результата
            if len(res) == 1:
                packages.append(res[0])

            elif len(res) and len(res) <= 20:
                log.group(f'Для {n} есть несколько вариантов:')
                
                if optional:
                    res += ["pass"]

                selected = select(res)

                if selected == "pass":
                    continue
                
                else:
                    packages.append(selected)

            else:
                log.warn(f'{n} - {len(res)} совпадений: {", ".join(res)}')

        return packages

    def packages_list(self, packages=[], mini=True, desc=True):
        """Отображает статусы сиска пакетов.
        
        :param packages: список PID пакетов
            если отцуцтвует - все пакеты индекса
        :param mini: Отоображать ли краткий статус пакета
        :param desc: Отображать ли описание пакета"""

        if not packages:
            packages = self.data.keys()

        for x in packages:
            self.get_package(x).status(mini, desc)

    def remove_packages(self, packages=[]):
        """Удаляет пакеты из системы.

        :param packages: список PID для удаления

        :return: Выполнена ли операция True/False"""

        packages = self.get_packages_by_pid(packages)
        
        for x in packages:
            used = self.data[x]["used"]

            for p in packages:
                if p in used:
                    used.remove(p)

            if used:
                log.warn(f'{x} требуется для {", ".join(self.data[x]["used"])}')
                packages.remove(x)

        if packages:
            self.packages_list(packages)
            if ask_user():
                for x in packages:
                    self.get_package(x).remove()
                    return True

    def clear_package(self, pid):
        res = self.data[pid]

        del res["used"]
        del res["url"]
        del res["multiple"]

        temp = []
        for x in res["require"]:
            temp.append(x.split('.', 1)[-1])

        res["require"] = temp

        return res
